package org.nkumar.immortal.web;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.IOException;
import java.util.Set;
import java.util.HashSet;

public final class ImmortalFilter implements Filter
{
    static final long ONE_YEAR_SECS = 365L * 24 * 60 * 60;
    static final long ONE_YEAR_MILLISECS = ONE_YEAR_SECS * 1000;
    //sets the max age to one year
    static final String CACHE_CONTROL_VALUE = "max-age=" + String.valueOf(ONE_YEAR_SECS);

    static final String EXPIRES_HEADER = "Expires";
    static final String CACHE_CONTROL_HEADER = "Cache-Control";
    static final String CONTENT_ENCODING_HEADER = "Content-Encoding";
    static final String IF_MODIFIED_SINCE_HEADER = "If-modified-since";
    static final String ETAG_HEADER = "ETag";

    private final Set<String> gzippedSet = new HashSet<String>();

    public void init(final FilterConfig filterConfig) throws ServletException
    {
        final String gzipExts = filterConfig.getInitParameter("gzipExts");
        if (gzipExts != null)
        {
            final String[] exts = gzipExts.split("[,]");
            for (final String ext : exts)
            {
                final String ext1 = ext.trim().toLowerCase();
                if (ext1.length() > 0)
                {
                    gzippedSet.add(ext1);
                }
            }
        }
    }

    /**
     * This filter takes care of the following three website performance rules.
     * <ul>
     * <li>Add an Expires Header</li>
     * <li>Gzip Components</li>
     * <li>Remove ETags</li>
     * </ul>
     * Adds cache control header with max age as one year.
     * If gzip has been enabled for some extensions then it added the content encoding header too.
     */
    public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse,
            final FilterChain filterChain) throws IOException, ServletException
    {
        final HttpServletRequest request = (HttpServletRequest) servletRequest;
        final HttpServletResponse response = (HttpServletResponse) servletResponse;

        //only GET and HEAD requests are allowed
        if (!isGetOrHeadRequest(request))
        {
            response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
            return;
        }

        //if the request has If-modified-since then return 304 response without any content
        //no need to check any timestamp. all the resources are immortal
        if (isConditionalGetRequest(request))
        {
            response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
            addCachingHeaders(response);
            return;
        }

        //if it is a regular request then add caching headers, and gzip headers
        //then pass it to the regular file handling servlet
        HttpServletResponse wrappedResponse = new ETagEvadingWrapper(response);
        addCachingHeaders(wrappedResponse);
        if (isZippedContent(request, gzippedSet))
        {
            response.addHeader(CONTENT_ENCODING_HEADER, "gzip");
        }
        filterChain.doFilter(servletRequest, wrappedResponse);
    }

    private static void addCachingHeaders(HttpServletResponse response)
    {
        response.addDateHeader(EXPIRES_HEADER, System.currentTimeMillis() + ONE_YEAR_MILLISECS);
        response.addHeader(CACHE_CONTROL_HEADER, CACHE_CONTROL_VALUE);
    }

    private static boolean isGetOrHeadRequest(final HttpServletRequest request)
    {
        final String method = request.getMethod();
        return "GET".equalsIgnoreCase(method) || "HEAD".equalsIgnoreCase(method);
    }

    private static boolean isConditionalGetRequest(final HttpServletRequest request)
    {
        try
        {
            return request.getDateHeader(IF_MODIFIED_SINCE_HEADER) != -1;
        }
        catch (Exception ignore)
        {
            return false;
        }
    }

    private static boolean isZippedContent(final HttpServletRequest request, final Set<String> gzippedSet)
    {
        boolean isZipped = false;
        final String url = request.getRequestURI();
        final int index = url.lastIndexOf('.');
        if (index != -1)
        {
            final String extension = url.substring(index + 1);
            if (gzippedSet.contains(extension))
            {
                isZipped = true;
            }
        }
        return isZipped;
    }

    public void destroy()
    {
    }

    /**
     * Prevent servers from setting the ETags.
     */
    static final class ETagEvadingWrapper extends HttpServletResponseWrapper
    {
        public ETagEvadingWrapper(HttpServletResponse httpServletResponse)
        {
            super(httpServletResponse);
        }

        @Override
        public void setHeader(String name, String value)
        {
            if (!ETAG_HEADER.equalsIgnoreCase(name))
            {
                super.setHeader(name, value);
            }
        }
    }
}
